Introduction

En este report nos enfocaremos en definir una estrategia de trading para el SP500; desarrollaremos por lo tanto modelos predictivos, en este caso de clasificación (sólo estamos interesados en el signo del return, no en el valor concreto que toma).

Nos quedaremos tan sólo con las observaciones posteriores al 1 de Diciembre de 1969, para evitar valores ausentes y demás. Es posible que más adelante decidamos quedarnos con un rango de datos aún inferior, pues es muy probable que los patrones de movimientos del precio de hace 40 años y los de ahora no tengan nada que ver (los mercados han evolucionado, los agentes del mercado han cambiado, las dinámicas internacionales entre los países también, la normativa etc.)

Las variables de que se compone nuestro dataset son:

  • Date = “Fecha”,
  • Index = “El nivel de precios del S&P500”,
  • D12 = “Sumas móviles a doce meses de los dividendos pagados por el índice S&P500”,
  • E12 = “Sumas móviles a doce meses del los beneficios del índice S&P 500”,
  • b.m = “Ratio Valor Contable/Precio (book to market)”, -tbl = “Intereses que pagan las letras del tesoro”,
  • AAA = “Rendimiento de los bonos triple A”,
  • BAA = “Rendimiento de los bonos BAA”,
  • lty = “Yield de los bonos a largo plazo del gobierno”,
  • ntis = “Aumento del patrimonio neto”,
  • Rfree = “La tasa libre de riesgo es la tasa de las letras del tesoro”,
  • infl = “Inflación, como Índice de Precios al Consumo(IPC)”,
  • ltr = “Retorno de los bonos a largo plazo del gobierno”, -corpr = “Retorno de los bonos a largo plazo de las empresas”,
  • svar = “Varianza del Stock”,
  • csp = “Cross-Sectional beta premium”,
  • CRSP_SPvw = “Rentabilidad (incluyendo dividendos) del S&P500 calculada por el CRSP”,
  • CRSP_SPvwx = “Rentabilidad (excluyendo dividendos) del S&P500 calculada por el CRSP”.

Análisis Exploratorio y Selección de Variables + Feature Engineering

Como vemos las correlaciones entre algunas de nuestras variables son altas; sin embargo el gráfico superior puede resultar engañoso, pues tenemos cada fila correspondiente al mismo momento temporal \(t\), mientras que nosotros queremos modelizar \(r_{t+1} = \beta_i \cdot X_t\) (en realidad vamos a modelizar solo el signo de \(r_{t+1}\)), siendo \(X_t\) las variables que elijamos para predecir dicho signo. Por este motivo, “recortaremos” nuestro dataset, para traer la variable CRSP_SPvwx una observación hacia atrás (haciendo así que el TARGET, que pertenece al momento \(t+1\) esté en la misma fila que las variables pertenecientes al momento \(t\)).

Ahora volvemos a realizar el gráfico de correlaciones, pero esta vez tenemos el TARGET de la forma que hemos especificado en el párrafo anterior, de tal forma que podemos ver la relación lineal que existe entre las variables predictoras en el momento \(t\) y la variable objetivo en el momento \(t+1\).

Como vemos, la correlación del TARGET en \(t+1\) con el resto de variables en \(t\) no es alta; solo las variables corpr, ltr, svar y csp tienen una correlación distinta de 0 con el TARGET, y ni siquiera se puede decir que ésta sea significativa.

Comenzaremos a crear algunas variables que en la literatura financiera, en especial en este link se menciona que son significativas a la hora de predecir el return del día siguiente.

Ahora volveremos a hacer el gráfico de correlaciones con estas nuevas variables que hemos introducido.

No parecemos encontrar variables que de verdad tenga una correlación alta, ya sea positiva o negativa, con el signo del precio del mes siguiente. Esto podría ser preocupante; vamos a realizar ahora un análisis algo más fino, en el que podamos encontrar relaciones entre las diferentes variables que teníamos más las que hemos creado y el TARGET.

Vemos que la variable csp tiene valores ausentes a partir del 2000 y algo, por lo que no creemos que sea buena idea utilizar esta variable. De hecho, hacemos un pequeño estudio de las variables que tienen valores ausentes y esto es lo que encontramos:

##       date      Index        D12        E12        b.m        tbl 
##          0          0          0          0          0          0 
##        AAA        BAA        lty       ntis      Rfree       infl 
##          0          0          0          0          0          0 
##        ltr      corpr       svar        csp  CRSP_SPvw CRSP_SPvwx 
##          0          0          0        167          0          0 
##     TARGET        dpt        dyt        ept        det 
##          0          0          0          0          0

Como podemos apreciar en este gráfico, hay dos o tres épocas de especial volatilidad en los mercados; estos son momentos pertenecientes a épocas de crisis. Podríamos incluir una variable binaria que capturara el efecto de la crisis; pero esto sería sesgar el modelo, porque en un setting real, no vamos a saber que estamos en crisis hasta que hayamos acumulado unas cuantas pérdidas en el mercado por caídas inesperadas grandes de las que éste no se recupera rápidamente.

Para continuar con el análisis, el TARGET lo vamos a transformar ya a 0 y 1 (baja o sube), de tal forma que podamos ver si ambas clases están balanceadas, además de examinar alguna posible relación con las variables predictivas que tenemos.

d2$TARGET = ifelse(d2$TARGET > 0, 1, 0)

Vemos que las clases están desbalanceadas, pues hay más días que el mercado experimenta subidas que bajadas. Esto podría sesgar al modelo, pues va a tener una tasa de error relativamente baja apostando siempre a que sube, pero un modelo de este tipo evidentemente perdería dinero. Por lo tanto tendremos que tener en cuenta esta característica de nuestro TARGET a la hora de modelizar, para equilibrar un poco más las clases. Utilizaremos además métricas como el AUC o F1, que son mucho más adecuadas que el Accuracy cuando tenemos clases desbalanceadas.

Aunque las diferencias no son excesivamente grandes, sí podemos ver que para corpr más grandes, generalmente hay más valores de 1 que de 0. Esto, de todas formas, ya decimos que es muy pequeño; a partir de 0.05 vemos algunas observaciones más en 1 que en 0, pero no suficiente como para que un clasificador pueda predecir decentemente.

En el caso de ltr, observamos algo parecido a lo que podíamos ver en el anterior gráfico de corpr, y es que para \(ltr \geq 0.05\) hay más oservaciones con TARGET = 1 que con TARGET = 0. En este caso hay aún más ruido que en corpr.

Por último, vamos a ver la relación entre el TARGET y svar, ya que ésta es la otra variable que tiene una \(\lvert \rho_{x, TARGET}} \geq 0.10\).

Parece que cuando la varianza es muy alta, al mes siguiente generalmente el mercado cae (TARGET = 0).

Vamos a crear un par de variables más, igual que hacen los autores del artículo mencionado anteriormente. En la primera de las mismas vamos a restar al long-term yield on government bonds el treasury bill rate; en el segundo caso, queremos obtener la diferencia entre el AAA rate y el BAA rate (el rate medio que tienen cada uno de estos grupos, según nivel de riesgo en deuda). En el chunk inferior se puede ver el código utilizado para crear estas dos variables.

d2$tms = d2$lty-d2$tbl

d2$dfy = log(d2$BAA/d2$AAA)

cor(d2$dfy, d2$TARGET)
## [1] 0.04114294
cor(d2$tms, d2$TARGET)
## [1] 0.06996692

Como vemos, las correlaciones de estas dos variables nuevas que hemos creado no son demasiado altas. Hemos probado tanto la opción del logaritmo de la división como la resta simple en ambos casos, quedándonos con la opción que mejor correlación dejaba en cada caso.

##       date                Index              D12              E12        
##  Min.   :1970-02-01   Min.   :  63.54   Min.   : 3.070   Min.   :  5.13  
##  1st Qu.:1981-10-08   1st Qu.: 125.68   1st Qu.: 6.566   1st Qu.: 14.18  
##  Median :1993-06-16   Median : 449.16   Median :12.520   Median : 22.41  
##  Mean   :1993-06-16   Mean   : 699.14   Mean   :14.724   Mean   : 35.04  
##  3rd Qu.:2005-02-22   3rd Qu.:1206.16   3rd Qu.:20.160   3rd Qu.: 51.35  
##  Max.   :2016-11-01   Max.   :2198.81   Max.   :45.476   Max.   :105.96  
##                                                                          
##       b.m              tbl               AAA               BAA         
##  Min.   :0.1205   Min.   :0.00010   Min.   :0.03280   Min.   :0.04220  
##  1st Qu.:0.2862   1st Qu.:0.01920   1st Qu.:0.05650   1st Qu.:0.06765  
##  Median :0.3834   Median :0.04965   Median :0.07550   Median :0.08375  
##  Mean   :0.4948   Mean   :0.04839   Mean   :0.07706   Mean   :0.08806  
##  3rd Qu.:0.7144   3rd Qu.:0.06815   3rd Qu.:0.08930   3rd Qu.:0.10200  
##  Max.   :1.2065   Max.   :0.16300   Max.   :0.15490   Max.   :0.17180  
##                                                                        
##       lty               ntis               Rfree          
##  Min.   :0.01750   Min.   :-0.057677   Min.   :8.333e-06  
##  1st Qu.:0.04883   1st Qu.:-0.002242   1st Qu.:1.600e-03  
##  Median :0.06810   Median : 0.011725   Median :4.137e-03  
##  Mean   :0.06918   Mean   : 0.009217   Mean   :4.033e-03  
##  3rd Qu.:0.08425   3rd Qu.: 0.025070   3rd Qu.:5.679e-03  
##  Max.   :0.14820   Max.   : 0.045747   Max.   :1.358e-02  
##                                                           
##       infl                ltr                corpr          
##  Min.   :-0.019153   Min.   :-0.112400   Min.   :-0.094900  
##  1st Qu.: 0.001247   1st Qu.:-0.011425   1st Qu.:-0.008075  
##  Median : 0.002965   Median : 0.007150   Median : 0.007000  
##  Mean   : 0.003311   Mean   : 0.007219   Mean   : 0.007299  
##  3rd Qu.: 0.005208   3rd Qu.: 0.024850   3rd Qu.: 0.022750  
##  Max.   : 0.018059   Max.   : 0.152300   Max.   : 0.156000  
##                                                             
##       svar                csp             CRSP_SPvw        
##  Min.   :0.0001503   Min.   :-0.00417   Min.   :-0.215795  
##  1st Qu.:0.0008340   1st Qu.:-0.00192   1st Qu.:-0.015624  
##  Median :0.0013608   Median :-0.00130   Median : 0.012023  
##  Mean   :0.0023867   Mean   :-0.00115   Mean   : 0.009375  
##  3rd Qu.:0.0023532   3rd Qu.:-0.00054   3rd Qu.: 0.037569  
##  Max.   :0.0709451   Max.   : 0.00235   Max.   : 0.168113  
##                      NA's   :167                           
##    CRSP_SPvwx            TARGET            dpt              dyt        
##  Min.   :-0.217944   Min.   :0.0000   Min.   :-4.524   Min.   :-4.531  
##  1st Qu.:-0.018527   1st Qu.:0.0000   1st Qu.:-3.961   1st Qu.:-3.954  
##  Median : 0.009288   Median :1.0000   Median :-3.574   Median :-3.571  
##  Mean   : 0.006810   Mean   :0.5943   Mean   :-3.616   Mean   :-3.610  
##  3rd Qu.: 0.035542   3rd Qu.:1.0000   3rd Qu.:-3.279   3rd Qu.:-3.279  
##  Max.   : 0.164814   Max.   :1.0000   Max.   :-2.753   Max.   :-2.751  
##                                                                        
##       ept              det               tms                dfy         
##  Min.   :-4.836   Min.   :-1.2442   Min.   :-0.03650   Min.   :0.06831  
##  1st Qu.:-3.095   1st Qu.:-0.9672   1st Qu.: 0.01090   1st Qu.:0.09887  
##  Median :-2.868   Median :-0.8461   Median : 0.02245   Median :0.12292  
##  Mean   :-2.831   Mean   :-0.7847   Mean   : 0.02078   Mean   :0.14207  
##  3rd Qu.:-2.509   3rd Qu.:-0.6320   3rd Qu.: 0.03150   3rd Qu.:0.16659  
##  Max.   :-1.899   Max.   : 1.3795   Max.   : 0.04550   Max.   :0.51241  
## 

Aquí podemos ver que casi todas nuestras variables están en una escala más o menos parecida. Además, en el summary de arriba se ven otras cosas como que el 59.43% de los returns son positivos. Ya habíamos comentado antes el tema del desbalanceo, pero no le habíamos puesto cifra.

En el caso de TMS si que somos capaces de ver alguna pequeña relación con el TARGET, en el sentido de que es algo más alto en general cuando el TARGET es 1 que cuando el TARGET es 0. En el caso de Difference Yield Spread (dfy) vemos algún efecto también en la misma dirección.

En el gráfico de arriba podemos ver por último la distribución de todas nuestras variables. Vemos que algunas se comportan más o menos como una Normal, con algo más de curtosis quizás como en el caso de CRSP_SPvwx, y otras parecen tener dos “cabezas”, como dos grupos con un bajón en los valores del centro.

Para seleccionar las variables con las que nos quedamos haremos primero un modelo de Regresión Logística con todas las variables y veremos cuáles son significativas. Antes partiremos los datos en Train y Test.

Fase de Train: 1970-02-01 / 2005-12-01

Fase de Test: 2006-01-01 / 2016-11-01

Al ser datos en el tiempo, no podemos utilizar métodos de validación cruzada como tal, pues ésta coge datos aleatoriamente y nuestros datos han de estar ordenados para poder predecir. La única forma de hacer validación cruzada con series temporales es hacer diferentes particiones pero ordenadas, e ir entrenando el modelo y prediciendo en el siguiente espacio temporal. Sin embargo en este caso, como veremos más adelante, lo que haremos será ajustar el modelo en una parte de train inicial y luego iremos corriendo esa ventana a lo largo del periodo de test, de tal manera que predices el TARGET y automáticamente recibes ese nuevo TARGET (como si estuvieras prediciendo siempre lo que va a ocurrir el próximo mes y en ese nuevo mes vuelves a ajustar el modelo y predecir de nuevo).

Vamos a escalar todas las variables (excepto la fecha), pues esto puede ayudar al modelo. Para ello utilizaremos la función \(MinMaxScaler = \frac{x - min(x)}{max(x) - min(x)}\). Esta función deja todos los valores entre 0 y 1.

## 
## Call:
## glm(formula = as.factor(TARGET) ~ ., family = binomial(link = "logit"), 
##     data = train)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -2.4352  -1.1584   0.7024   0.9929   2.0712  
## 
## Coefficients: (3 not defined because of singularities)
##               Estimate Std. Error z value Pr(>|z|)  
## (Intercept)  -12.40385   13.94699  -0.889   0.3738  
## Index          0.94887    4.85512   0.195   0.8451  
## D12            1.29642    6.65961   0.195   0.8457  
## E12            2.51919    5.15852   0.488   0.6253  
## b.m           -4.17250    2.44839  -1.704   0.0883 .
## tbl            0.01261    1.63516   0.008   0.9938  
## AAA          -22.33287   21.95441  -1.017   0.3090  
## BAA           25.14942   19.85560   1.267   0.2053  
## lty           -7.49936    5.54966  -1.351   0.1766  
## ntis           0.35913    0.80676   0.445   0.6562  
## Rfree               NA         NA      NA       NA  
## infl          -3.59773    1.56456  -2.300   0.0215 *
## ltr           -2.65929    2.49061  -1.068   0.2856  
## corpr          1.44928    2.71613   0.534   0.5936  
## svar          -2.60174    2.75037  -0.946   0.3442  
## CRSP_SPvw      4.18052   22.65608   0.185   0.8536  
## CRSP_SPvwx    20.37103   32.75652   0.622   0.5340  
## dpt          123.32018  115.41957   1.068   0.2853  
## dyt         -117.06827  116.09307  -1.008   0.3133  
## ept            2.91671    4.94902   0.589   0.5556  
## det                 NA         NA      NA       NA  
## tms                 NA         NA      NA       NA  
## dfy           -4.72476    6.64442  -0.711   0.4770  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 585.07  on 430  degrees of freedom
## Residual deviance: 541.49  on 411  degrees of freedom
## AIC: 581.49
## 
## Number of Fisher Scoring iterations: 4

Parece que la única variable que es significativa (aparte del Intercept) es la inflación. Nos dará de todas formas un poco igual si es o no significativa estadísticamente la variable, lo que queremos es maximizar el accuracy y, más concretamente, medidas como el AUC o F1. Seguiremos dándole alguna vuelta más a los datos para limpiarles un poco más el ruido antes de comenzar a modelizar. Nos aparecen NAs, lo cual puede indicar que tenemos un problema de singularidad de la matriz, según vemos aquí. Esto es lo que podría estar causando que salgan valores ausentes, y lo que indica es que tenemos mucha colinearidad. Ya vimos en los gráficos de correlación iniciales que había correlaciones altas entre algunas variables, de tal forma que éstas podrían contener información redundante, y el modelo no es capaz de ajustarse con la matriz entera.

Comprobamos mediante el error que nos da el cálculo del VIF, que tenemos multicolinearidad perfecta. Para solventar esto, vamos a utilizar step wise logistic regression, de tal forma que seleccionaremos sólo algunas de las variables, quitándonos las que peor AIC dejen al ser introducidas. Esperamos que esto elimine las variables demasiado correlacionadas. Antes de comenzar a hacer esto, haremos un último cambio en nuestro dataset. Vamos a meter el lag 1, lag 2 y lag 3. También añadiremos la inflación del período lag1, y el b.m ratio (estaba cerca de ser significativa en la regresión logística).

lag1 = c(NA, NA, NA)
lag2 = c(NA, NA, NA)
lag3 = c(NA, NA, NA)

bm1 = c(NA, NA, NA)
infl1 = c(NA, NA, NA)


for(i in 4:nrow(d2)){
  
  lag1 = c(lag1, d2[i-1, "CRSP_SPvwx"])
  
  lag2 = c(lag2, d2[i-2, "CRSP_SPvwx"])
  
  lag3 = c(lag3, d2[i-3, "CRSP_SPvwx"])
  
  bm1 = c(bm1, d2[i-1, 'b.m'])
  
  infl1 = c(infl1, d2[i-1, "infl"])

}

d2$lag1 = lag1
d2$lag2 = lag2
d2$lag3 = lag3
d2$bm1 = bm1
d2$infl1 = infl1

d3 = d2[4:nrow(d2), ]

Ampliamos un mes nuestro train y se lo quitamos al test, en compensación por las tres filas perdidas para introducir los lags.

Ahora procedemos a hacer step-wise logistic regression:

## 
## Call:
## glm(formula = TARGET ~ E12 + b.m + BAA + lty + infl + ltr + dpt, 
##     family = binomial(link = "logit"), data = train)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -2.5181  -1.1367   0.7304   0.9928   2.1639  
## 
## Coefficients:
##             Estimate Std. Error z value Pr(>|z|)    
## (Intercept)    1.435      1.061   1.352 0.176230    
## E12            5.314      1.404   3.784 0.000155 ***
## b.m           -3.245      1.077  -3.013 0.002586 ** 
## BAA            6.754      2.976   2.269 0.023260 *  
## lty          -10.656      3.568  -2.986 0.002826 ** 
## infl          -3.364      1.455  -2.312 0.020777 *  
## ltr           -1.972      1.067  -1.848 0.064553 .  
## dpt            7.971      1.550   5.141 2.73e-07 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 581.54  on 428  degrees of freedom
## Residual deviance: 540.63  on 421  degrees of freedom
## AIC: 556.63
## 
## Number of Fisher Scoring iterations: 4

Como vemos, ha sido una forma de limpiar un poco el rudio, pues ahora tenemos variables claramente sinificativas en un modelo mucho más reducido libre de overfitting y de multicolinearidad. Podemos usar otro tipo de AIC (antes utilizamos el de ambas direcciones); por ejemplo:

library(MASS)

nullmod = glm(TARGET ~1, family = binomial(link='logit'), data = train)

stepmod = fullmod %>% stepAIC(scope=list(fullmod, nullmod), trace = FALSE, direction = 'backward')

summary(stepmod)
## 
## Call:
## glm(formula = TARGET ~ E12 + b.m + BAA + lty + infl + ltr + dpt, 
##     family = binomial(link = "logit"), data = train)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -2.5181  -1.1367   0.7304   0.9928   2.1639  
## 
## Coefficients:
##             Estimate Std. Error z value Pr(>|z|)    
## (Intercept)    1.435      1.061   1.352 0.176230    
## E12            5.314      1.404   3.784 0.000155 ***
## b.m           -3.245      1.077  -3.013 0.002586 ** 
## BAA            6.754      2.976   2.269 0.023260 *  
## lty          -10.656      3.568  -2.986 0.002826 ** 
## infl          -3.364      1.455  -2.312 0.020777 *  
## ltr           -1.972      1.067  -1.848 0.064553 .  
## dpt            7.971      1.550   5.141 2.73e-07 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 581.54  on 428  degrees of freedom
## Residual deviance: 540.63  on 421  degrees of freedom
## AIC: 556.63
## 
## Number of Fisher Scoring iterations: 4

Vemos que los resultados son exactamente iguales que cuando usábamos ambas direcciones, por lo tanto parece que este es el modelo con el que empezaremos. Posteriormente valoraremos incluir alguna otra variable. Resulta especialmente llamativo que no se encuentren conexiones entre los lags 1, 2 y 3 (y 4, realmente son 4 porque el lag1 ya estaba incluido, pues recordemos que el TARGET lo trajimos una observación hacia arriba, de esta forma dejando como variable predictiva el Return en t) del Return y el TARGET.

Una forma alternativa para el feature selection es utilizando la librería caret:

Según lo que vemos, el RMSE de media bajaría en la validación cruzada al incluir todas las variables. Planteamos por tanto dos modelos, el que nos sugiere el Step-Wise y el que nos sugiere el método de caret. A este método de caret de todas formas le quitaremos algunas de las variables para evitar problemas graves de colinearidad perfecta.

y_true = test$TARGET
train$TARGET = as.factor(train$TARGET)
test$TARGET = as.factor(test$TARGET)

mod1 = TARGET ~ Index + b.m + tbl + ntis + infl + ltr + svar + CRSP_SPvwx + dpt + BAA + lty + lag1 + lag2 + lag3

mod2 = TARGET ~ E12 + b.m + BAA + lty + infl + ltr + dpt

Modelización: Clasificación.

Crearemos un dataframe en el que ir guardando los resultados en términos para cada uno de los métodos que utilicemos con cada modelo, para así más adelante poder comparar los resultados. La medida que utilizaremos será \(F1 = \frac{2 \cdot precision \cdot recall}{precision + recall}\), pues es una buena medida para comparar modelos. Para ello utilizaremos el paquete MLmetrics. Nota: Cuando tengamos una clase que no ha sido predicha nunca, el F1 Score nos dará NaN, ya que habrá divisiones entre 0 en su cálculo y eso da NaN; probablemente más adelante tengamos que transformar estos valores a 0 directamente para poder realizar comparaciones.

Rectificación: Desistimos de usar F1_Score por el alto número de errores que da. Estaría bien utilizarla para comparar modelos pues es una buena medida comparativa, pero como en la mayoría de modelos vemos que la clase 0 apenas se predice (lo cual causa los errores de cálculo que mencionaba antes), consideramos que es mejor utilizar el Kappa, pues es un valor que podemos obtener para todos los modelos y de esta forma podemos compararlos mejor. Decidimos utilizar Kappa porque es una medida que contrasta el Accuracy obtenido por el modelo con el que tendría la “predicción tonta”, por eso creemos que para este problema en concreto puede funcionar bastante bien; además siempre tenemos un valor de la misma, aunque sea de 0, por lo que no nos encontraremos con los problemas planteados por la métrica F1. Cuanto más grande sea el Kappa, mejor el modelo.

Sobra decir que esta medida de error se calcula en el test, nos da igual lo bien que se ajuste en el train, lo que queremos es maximizar lo bien que predice los datos que aún no ha visto para poder así diseñar una estrategia exitosa en bolsa.

Linear Discriminant Analysis

With Model 1

## Linear Discriminant Analysis 
## 
## 429 samples
##  14 predictor
##   2 classes: '0', '1' 
## 
## Pre-processing: centered (14), scaled (14) 
## Resampling: Cross-Validated (5 fold, repeated 10 times) 
## Summary of sample sizes: 343, 343, 344, 342, 344, 344, ... 
## Resampling results:
## 
##   Accuracy   Kappa    
##   0.6125511  0.1640448
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0  0  0
##          1 49 81
##                                           
##                Accuracy : 0.6231          
##                  95% CI : (0.5339, 0.7065)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.539           
##                                           
##                   Kappa : 0               
##  Mcnemar's Test P-Value : 7.025e-12       
##                                           
##             Sensitivity : 0.0000          
##             Specificity : 1.0000          
##          Pos Pred Value :    NaN          
##          Neg Pred Value : 0.6231          
##              Prevalence : 0.3769          
##          Detection Rate : 0.0000          
##    Detection Prevalence : 0.0000          
##       Balanced Accuracy : 0.5000          
##                                           
##        'Positive' Class : 0               
## 

Vemos que este clasificador no es en absoluto mejor que el Naive Classifier, es decir, aquél que diría siempre que el Return va a ser positivo; esto lo podemos ver en la matriz de confusión superior, en la cual se aprecia que no hay ninguna observación del test para la que la predicción sea 0. Esto podríamos mejorarlo cambiando las probabilidades a priori que tiene cada clase, de esta forma sesgamos un poco el modelo pero nos equivocaremos menos los días que baje la bolsa, de esta forma perderíamos menos dinero.

With Model 2

## Shrinkage Discriminant Analysis 
## 
## 429 samples
##   7 predictor
##   2 classes: '0', '1' 
## 
## Pre-processing: centered (7), scaled (7) 
## Resampling: Cross-Validated (5 fold, repeated 10 times) 
## Summary of sample sizes: 344, 342, 344, 343, 343, 342, ... 
## Resampling results across tuning parameters:
## 
##   lambda  Accuracy   Kappa    
##   0.0     0.6354171  0.2206779
##   0.5     0.6032406  0.1021078
##   1.0     0.5829337  0.1031499
## 
## Tuning parameter 'diagonal' was held constant at a value of FALSE
## Accuracy was used to select the optimal model using the largest value.
## The final values used for the model were diagonal = FALSE and lambda = 0.
## Prediction uses 7 features.
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0  0  0
##          1 49 81
##                                           
##                Accuracy : 0.6231          
##                  95% CI : (0.5339, 0.7065)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.539           
##                                           
##                   Kappa : 0               
##  Mcnemar's Test P-Value : 7.025e-12       
##                                           
##             Sensitivity : 0.0000          
##             Specificity : 1.0000          
##          Pos Pred Value :    NaN          
##          Neg Pred Value : 0.6231          
##              Prevalence : 0.3769          
##          Detection Rate : 0.0000          
##    Detection Prevalence : 0.0000          
##       Balanced Accuracy : 0.5000          
##                                           
##        'Positive' Class : 0               
## 

Biasing the Model for Capturing Downtrends

Para sesgar al modelo y conseguir que capte algunas bajadas del precio y no se convierta en el Naive Predictor (Aquél que tendría un \(Accuracy = 0.58\) sólo por decir siempre que el precio sube) vamos a poner que las probabilidades a posteriori son 0.9 para el 0 y 0.1 para el 1 (después de probar con varios valores estos han sido los únicos que nos daban un resultado efectivo a la hora de conseguir predecir bien algunos 0).

##         Length Class  Mode     
## prior    2     -none- numeric  
## counts   2     -none- numeric  
## means   28     -none- numeric  
## scaling 14     -none- numeric  
## lev      2     -none- character
## svd      1     -none- numeric  
## N        1     -none- numeric  
## call     4     -none- call     
## terms    3     terms  call     
## xlevels  0     -none- list
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0 16 21
##          1 33 60
##                                           
##                Accuracy : 0.5846          
##                  95% CI : (0.4949, 0.6703)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.8403          
##                                           
##                   Kappa : 0.0707          
##  Mcnemar's Test P-Value : 0.1344          
##                                           
##             Sensitivity : 0.3265          
##             Specificity : 0.7407          
##          Pos Pred Value : 0.4324          
##          Neg Pred Value : 0.6452          
##              Prevalence : 0.3769          
##          Detection Rate : 0.1231          
##    Detection Prevalence : 0.2846          
##       Balanced Accuracy : 0.5336          
##                                           
##        'Positive' Class : 0               
## 

Vemos que de esta forma somos capaces de predecir algunos 0 (el precio baja), pero sigue sin ser suficiente; parece que aún no encontramos un modelo que pueda captar bien las tendencias de los precios.

Quadratic Discriminant Analysis

With Model 1

## Quadratic Discriminant Analysis 
## 
## 429 samples
##  14 predictor
##   2 classes: '0', '1' 
## 
## Pre-processing: centered (14), scaled (14) 
## Resampling: Cross-Validated (5 fold, repeated 10 times) 
## Summary of sample sizes: 342, 344, 344, 342, 344, 344, ... 
## Resampling results:
## 
##   Accuracy   Kappa     
##   0.5797283  0.07732432
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0 34 49
##          1 15 32
##                                           
##                Accuracy : 0.5077          
##                  95% CI : (0.4186, 0.5964)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.9972          
##                                           
##                   Kappa : 0.0782          
##  Mcnemar's Test P-Value : 3.707e-05       
##                                           
##             Sensitivity : 0.6939          
##             Specificity : 0.3951          
##          Pos Pred Value : 0.4096          
##          Neg Pred Value : 0.6809          
##              Prevalence : 0.3769          
##          Detection Rate : 0.2615          
##    Detection Prevalence : 0.6385          
##       Balanced Accuracy : 0.5445          
##                                           
##        'Positive' Class : 0               
## 

Vemos que QDA tiene una ventaja sobre LDA en el sentido de que los errores no están todos concentrados en la clase minoritaria, sino que se reparten. Esto significa que también predecimos considerablemente bien cuando el precio del mercado va a bajar. Sin embargo, el Accuracy baja bastante.

With Model 2

## Quadratic Discriminant Analysis 
## 
## 429 samples
##   7 predictor
##   2 classes: '0', '1' 
## 
## Pre-processing: centered (7), scaled (7) 
## Resampling: Cross-Validated (5 fold, repeated 10 times) 
## Summary of sample sizes: 342, 343, 344, 344, 343, 344, ... 
## Resampling results:
## 
##   Accuracy   Kappa    
##   0.6223565  0.1982167
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0  7  9
##          1 42 72
##                                           
##                Accuracy : 0.6077          
##                  95% CI : (0.5182, 0.6921)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.6766          
##                                           
##                   Kappa : 0.0366          
##  Mcnemar's Test P-Value : 7.433e-06       
##                                           
##             Sensitivity : 0.14286         
##             Specificity : 0.88889         
##          Pos Pred Value : 0.43750         
##          Neg Pred Value : 0.63158         
##              Prevalence : 0.37692         
##          Detection Rate : 0.05385         
##    Detection Prevalence : 0.12308         
##       Balanced Accuracy : 0.51587         
##                                           
##        'Positive' Class : 0               
## 

El Kappa en este caso baja algo con respecto a LDA en el test, aunque el Accuracy sube un poco. Lo que parece claro es que con QDA el segundo modelo generaliza mejor que el primero.

Naive Bayes

With Model 1

## Naive Bayes 
## 
## 429 samples
##  14 predictor
##   2 classes: '0', '1' 
## 
## Pre-processing: centered (14), scaled (14) 
## Resampling: Cross-Validated (5 fold, repeated 10 times) 
## Summary of sample sizes: 344, 343, 344, 342, 343, 342, ... 
## Resampling results across tuning parameters:
## 
##   usekernel  Accuracy   Kappa     
##   FALSE      0.5692831  0.02926108
##    TRUE      0.5677084  0.09639008
## 
## Tuning parameter 'fL' was held constant at a value of 0
## Tuning
##  parameter 'adjust' was held constant at a value of 1
## Accuracy was used to select the optimal model using the largest value.
## The final values used for the model were fL = 0, usekernel = FALSE
##  and adjust = 1.
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0 17 16
##          1 32 65
##                                           
##                Accuracy : 0.6308          
##                  95% CI : (0.5417, 0.7137)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.46691         
##                                           
##                   Kappa : 0.1597          
##  Mcnemar's Test P-Value : 0.03038         
##                                           
##             Sensitivity : 0.3469          
##             Specificity : 0.8025          
##          Pos Pred Value : 0.5152          
##          Neg Pred Value : 0.6701          
##              Prevalence : 0.3769          
##          Detection Rate : 0.1308          
##    Detection Prevalence : 0.2538          
##       Balanced Accuracy : 0.5747          
##                                           
##        'Positive' Class : 0               
## 

With Model 2

## Naive Bayes 
## 
## 429 samples
##   7 predictor
##   2 classes: '0', '1' 
## 
## Pre-processing: centered (7), scaled (7) 
## Resampling: Cross-Validated (5 fold, repeated 10 times) 
## Summary of sample sizes: 342, 343, 343, 344, 344, 344, ... 
## Resampling results across tuning parameters:
## 
##   usekernel  Accuracy   Kappa     
##   FALSE      0.5692575  0.05679631
##    TRUE      0.5699445  0.10154103
## 
## Tuning parameter 'fL' was held constant at a value of 0
## Tuning
##  parameter 'adjust' was held constant at a value of 1
## Accuracy was used to select the optimal model using the largest value.
## The final values used for the model were fL = 0, usekernel = TRUE
##  and adjust = 1.
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0  2  1
##          1 47 80
##                                           
##                Accuracy : 0.6308          
##                  95% CI : (0.5417, 0.7137)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.4669          
##                                           
##                   Kappa : 0.035           
##  Mcnemar's Test P-Value : 8.293e-11       
##                                           
##             Sensitivity : 0.04082         
##             Specificity : 0.98765         
##          Pos Pred Value : 0.66667         
##          Neg Pred Value : 0.62992         
##              Prevalence : 0.37692         
##          Detection Rate : 0.01538         
##    Detection Prevalence : 0.02308         
##       Balanced Accuracy : 0.51424         
##                                           
##        'Positive' Class : 0               
## 

Parece que en general funcionan mejor los algoritmos con el segundo modelo que con el primero; esto se puede deber a que el primero sigue teniendo problemas de muticolinearidad pese a haber eliminado ya algunas de las columnas que estaban produciendo esto. Esto en Naive Bayes no queda tan claro, pues en el segundo caso tanto el Kappa es menor que con el primer modelo. Vemos además que Naive Bayes predice algo mejor que los métodos utilizados anteriormente. De todas formas, la comparación de los modelos se realizará más adelante, por lo que ahora no nos centraremos tanto en compararlos.

GLMNet

With Model 1:

## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0  0  0
##          1 49 81
##                                           
##                Accuracy : 0.6231          
##                  95% CI : (0.5339, 0.7065)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.539           
##                                           
##                   Kappa : 0               
##  Mcnemar's Test P-Value : 7.025e-12       
##                                           
##             Sensitivity : 0.0000          
##             Specificity : 1.0000          
##          Pos Pred Value :    NaN          
##          Neg Pred Value : 0.6231          
##              Prevalence : 0.3769          
##          Detection Rate : 0.0000          
##    Detection Prevalence : 0.0000          
##       Balanced Accuracy : 0.5000          
##                                           
##        'Positive' Class : 0               
## 

With Model 2:

## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0  0  0
##          1 49 81
##                                           
##                Accuracy : 0.6231          
##                  95% CI : (0.5339, 0.7065)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.539           
##                                           
##                   Kappa : 0               
##  Mcnemar's Test P-Value : 7.025e-12       
##                                           
##             Sensitivity : 0.0000          
##             Specificity : 1.0000          
##          Pos Pred Value :    NaN          
##          Neg Pred Value : 0.6231          
##              Prevalence : 0.3769          
##          Detection Rate : 0.0000          
##    Detection Prevalence : 0.0000          
##       Balanced Accuracy : 0.5000          
##                                           
##        'Positive' Class : 0               
## 

Linear (Kernel) Support Vector Machine

With Model 1

## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0  0  0
##          1 49 81
##                                           
##                Accuracy : 0.6231          
##                  95% CI : (0.5339, 0.7065)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.539           
##                                           
##                   Kappa : 0               
##  Mcnemar's Test P-Value : 7.025e-12       
##                                           
##             Sensitivity : 0.0000          
##             Specificity : 1.0000          
##          Pos Pred Value :    NaN          
##          Neg Pred Value : 0.6231          
##              Prevalence : 0.3769          
##          Detection Rate : 0.0000          
##    Detection Prevalence : 0.0000          
##       Balanced Accuracy : 0.5000          
##                                           
##        'Positive' Class : 0               
## 

With Model 2

## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0  0  0
##          1 49 81
##                                           
##                Accuracy : 0.6231          
##                  95% CI : (0.5339, 0.7065)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.539           
##                                           
##                   Kappa : 0               
##  Mcnemar's Test P-Value : 7.025e-12       
##                                           
##             Sensitivity : 0.0000          
##             Specificity : 1.0000          
##          Pos Pred Value :    NaN          
##          Neg Pred Value : 0.6231          
##              Prevalence : 0.3769          
##          Detection Rate : 0.0000          
##    Detection Prevalence : 0.0000          
##       Balanced Accuracy : 0.5000          
##                                           
##        'Positive' Class : 0               
## 

En el caso de SVM con Kernel Lineal, vemos que con ninguno de los dos modelos es captar de separar bien la muestra; no predice ningún 0.

SVM Radial

Ahora probaremos con otro tipo de Support Vector Machine, con un Kernel Radial, expandiéndole el grid para que pueda optimizar sus hiperparámetros.

With Model 1

## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0  3  3
##          1 46 78
##                                           
##                Accuracy : 0.6231          
##                  95% CI : (0.5339, 0.7065)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.539           
##                                           
##                   Kappa : 0.0293          
##  Mcnemar's Test P-Value : 1.973e-09       
##                                           
##             Sensitivity : 0.06122         
##             Specificity : 0.96296         
##          Pos Pred Value : 0.50000         
##          Neg Pred Value : 0.62903         
##              Prevalence : 0.37692         
##          Detection Rate : 0.02308         
##    Detection Prevalence : 0.04615         
##       Balanced Accuracy : 0.51209         
##                                           
##        'Positive' Class : 0               
## 

With Model 2

## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0  1  3
##          1 48 78
##                                           
##                Accuracy : 0.6077          
##                  95% CI : (0.5182, 0.6921)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.6766          
##                                           
##                   Kappa : -0.0203         
##  Mcnemar's Test P-Value : 7.218e-10       
##                                           
##             Sensitivity : 0.020408        
##             Specificity : 0.962963        
##          Pos Pred Value : 0.250000        
##          Neg Pred Value : 0.619048        
##              Prevalence : 0.376923        
##          Detection Rate : 0.007692        
##    Detection Prevalence : 0.030769        
##       Balanced Accuracy : 0.491686        
##                                           
##        'Positive' Class : 0               
## 

En el caso de SVM con Kernel Radial, el modelo 1 funciona mejor que el modelo 2, pues como vemos el Kappa es mayor en el primer caso, y distinto de 0, lo cual es un pequeño avance con respecto a la mayoría de métodos que vimos anteriormente.

KNN

With Model 1

## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0  9  8
##          1 40 73
##                                           
##                Accuracy : 0.6308          
##                  95% CI : (0.5417, 0.7137)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.4669          
##                                           
##                   Kappa : 0.0975          
##  Mcnemar's Test P-Value : 7.66e-06        
##                                           
##             Sensitivity : 0.18367         
##             Specificity : 0.90123         
##          Pos Pred Value : 0.52941         
##          Neg Pred Value : 0.64602         
##              Prevalence : 0.37692         
##          Detection Rate : 0.06923         
##    Detection Prevalence : 0.13077         
##       Balanced Accuracy : 0.54245         
##                                           
##        'Positive' Class : 0               
## 

Vemos que con el modelo 1 KNN funciona bastante bien, mejor que muchos de los métodos examinados hasta ahora, y mejor por ejemplo que SVM Lineal, que en teoría es un algoritmo más sofisticado.

With Model 2

## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0  2  6
##          1 47 75
##                                           
##                Accuracy : 0.5923          
##                  95% CI : (0.5027, 0.6776)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.793           
##                                           
##                   Kappa : -0.0398         
##  Mcnemar's Test P-Value : 3.92e-08        
##                                           
##             Sensitivity : 0.04082         
##             Specificity : 0.92593         
##          Pos Pred Value : 0.25000         
##          Neg Pred Value : 0.61475         
##              Prevalence : 0.37692         
##          Detection Rate : 0.01538         
##    Detection Prevalence : 0.06154         
##       Balanced Accuracy : 0.48337         
##                                           
##        'Positive' Class : 0               
## 

Sin embargo con el segundo modelo el rendimiento de KNN baja considerablemente, pues el Kappa es incluso negativo. Vemos que en este caso sería mejor por lo tanto utilizar más variables. De nuevo, vemos que los problemas reales de utilizar muchas variables vienen por el lado de la multicolinearidad, y que los algoritmos que no necesitan calcular determinantes de la matriz ni nada por el estilo (en el caso del KNN lo que se hace es calcular una matriz de distancias multidimensional entre los puntos y se le otorga al punto nuevo el valor de lo más “votado” entre los puntos con los que tiene mayor cercanía – como vemos, nada tiene que ver esto con calcular un determinante) se aprovechan de la ganancia de información que viene por el lado de la inclusión de más variables.

Neural Networks

With Model 1

## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0  0  0
##          1 49 81
##                                           
##                Accuracy : 0.6231          
##                  95% CI : (0.5339, 0.7065)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.539           
##                                           
##                   Kappa : 0               
##  Mcnemar's Test P-Value : 7.025e-12       
##                                           
##             Sensitivity : 0.0000          
##             Specificity : 1.0000          
##          Pos Pred Value :    NaN          
##          Neg Pred Value : 0.6231          
##              Prevalence : 0.3769          
##          Detection Rate : 0.0000          
##    Detection Prevalence : 0.0000          
##       Balanced Accuracy : 0.5000          
##                                           
##        'Positive' Class : 0               
## 

Con las variables del Modelo 1 parece que las redes neuronales planteadas no funcionan demasiado allá, pues como vemos tienen un Kappa de 0. Veamos qué tal funcionan con el Modelo 2.

With Model 2

## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0  0  0
##          1 49 81
##                                           
##                Accuracy : 0.6231          
##                  95% CI : (0.5339, 0.7065)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.539           
##                                           
##                   Kappa : 0               
##  Mcnemar's Test P-Value : 7.025e-12       
##                                           
##             Sensitivity : 0.0000          
##             Specificity : 1.0000          
##          Pos Pred Value :    NaN          
##          Neg Pred Value : 0.6231          
##              Prevalence : 0.3769          
##          Detection Rate : 0.0000          
##    Detection Prevalence : 0.0000          
##       Balanced Accuracy : 0.5000          
##                                           
##        'Positive' Class : 0               
## 

Vemos que tampoco mejoran las redes neuronales para el modelo reducido en variables; el Kappa vuelve a ser 0 por lo que vemos que no es un buen modelo para este problema que queremos resolver.

Advanced Models

Hay otros modelos que llevan incorporada la selección de variables de una forma u de otra, por ejemplo el Random Forest o el XGB. ¿En qué sentido tienen incorporada la selección de variables? Bien, en el hecho de que no utilizan todas las variables en los árboles de decisión que los forman para realizar los cortes en los datos de cada una de las ramas del árbol; de esta forma, podemos incluso sacar un feature importances que nos indica el impacto teórico de cada variable en el conjunto de árboles que forman el modelo de ensemble a la hora de realizar los cortes. Básicamente esto es un indicador del número de veces (de forma relativa) que ha aparecido cada variable en los cortes de los árboles. De esta forma, a la hora de aplicar modelos de este tipo es posible que los problemas de colinearidad desaparezcan y que tenga más sentido pasarles un modelo con todos los datos; pues de todas formas las variables que no tengan nada que decir sobre el TARGET no aparecerán en los árboles (esto es especialmente cierto en el XGB, en el que cada nuevo estimador (árbol) del ensemble se diseña teniendo en cuenta cómo están construidos y la información que contienen los árboles creados anteriormente).

Creamos por tanto una tercera modalidad de modelo, modfull, para este tipo de métodos de clasificación.

La implementación tanto del Random Forest como del resto de modelos de Ensemble se llevará a cabo en Python utilizando la librería sklearn y XGBoost, que nos permite hacer mejor y más fácilmente el hyperparameter tuning, además de ser mucho más rápido el cálculo. Los resultados de estos dos algoritmos se podrán ver más adelante al realizar las gráficas y demás.

write.csv(train, 'train.csv', sep = ',', col.names=T)
write.csv(test, 'test.csv', sep = ',', col.names=T)

A continuación mostraremos la matriz de confusión para Random Forest, XGB y Adaboost.

Random Forest

## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0  0  0
##          1 49 81
##                                           
##                Accuracy : 0.6231          
##                  95% CI : (0.5339, 0.7065)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.539           
##                                           
##                   Kappa : 0               
##  Mcnemar's Test P-Value : 7.025e-12       
##                                           
##             Sensitivity : 0.0000          
##             Specificity : 1.0000          
##          Pos Pred Value :    NaN          
##          Neg Pred Value : 0.6231          
##              Prevalence : 0.3769          
##          Detection Rate : 0.0000          
##    Detection Prevalence : 0.0000          
##       Balanced Accuracy : 0.5000          
##                                           
##        'Positive' Class : 0               
## 

El Random Forest curiosamente (digo curiosamente porque es uno de los algoritmos que mejor funcionan normalmente a la hora de predecir) tiene un Kappa de 0, es decir, que no es mejor que los métodos más simples de clasificación que vimos anteriormente.

XGBoost (Xtreme Gradient Boosting)

## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0 14 27
##          1 35 54
##                                           
##                Accuracy : 0.5231          
##                  95% CI : (0.4337, 0.6114)
##     No Information Rate : 0.6231          
##     P-Value [Acc > NIR] : 0.9921          
##                                           
##                   Kappa : -0.0492         
##  Mcnemar's Test P-Value : 0.3740          
##                                           
##             Sensitivity : 0.2857          
##             Specificity : 0.6667          
##          Pos Pred Value : 0.3415          
##          Neg Pred Value : 0.6067          
##              Prevalence : 0.3769          
##          Detection Rate : 0.1077          
##    Detection Prevalence : 0.3154          
##       Balanced Accuracy : 0.4762          
##                                           
##        'Positive' Class : 0               
## 

XGBoost, otro de los modelos más utilizados para predicción (pues normalmente su capacidad predictiva es alta), tiene una puntuación negativa en el Kappa score…

AdaBoost

## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0  7 13
##          1 42 68
##                                          
##                Accuracy : 0.5769         
##                  95% CI : (0.4872, 0.663)
##     No Information Rate : 0.6231         
##     P-Value [Acc > NIR] : 0.8797927      
##                                          
##                   Kappa : -0.02          
##  Mcnemar's Test P-Value : 0.0001597      
##                                          
##             Sensitivity : 0.14286        
##             Specificity : 0.83951        
##          Pos Pred Value : 0.35000        
##          Neg Pred Value : 0.61818        
##              Prevalence : 0.37692        
##          Detection Rate : 0.05385        
##    Detection Prevalence : 0.15385        
##       Balanced Accuracy : 0.49118        
##                                          
##        'Positive' Class : 0              
## 

Y resulta que Adaboost, otro modelo de Ensemble, también tiene un Kappa por debajo de 0. Parece, por tanto, que ninguno de los modelos de clasificación avanzada utilizados es realmente útil a la hora de predecir el signo del retorno del SP500 en el siguiente mes.

Visualizing Results

En esta sección, nos centraremos en analizar los resultados de todos los modelos de forma visual, de tal forma que podremos quedarnos con uno o dos de ellos para la parte final, en la cual diseñaremos una estrategia de Trading para el SP500.

Vemos que curiosamente KNN, uno de los algoritmos más simples, con el Modelo 1 (el que tiene muchas de las variables incluidas, solo con algunas ausencias) es el que mejor predice en el test con respecto a la predicción “Naive” o “tonta”. Como mencionamos antes, hemos utilizado el Kappa en lugar de Accuracy porque es una medida que contrasta el Accuracy obtenido por el modelo con el Accuracy que se obtendría de forma espúrica digamos. De esta forma tenemos algunos modelos como Naive Bayes con el Modelo 1 o XGB que tienen Kappa negativo (su accuracy está por debajo de lo que cabría esperar de una predicción tonta). Después de KNN con el Modelo 1, el siguiente que mejor funciona es QDA con el modelo 1, y después LDA Bis con el Modelo 1. Podríamos, por tanto, coger estos tres modelos para la fase final, es decir la de selección de una estrategia de trading para el SP500. Podríamos incluso utilizar los 3 modelos al mismo tiempo, cogiendo el voto mayoritario en cada caso de las 3 predicciones. De momento cogemos estos 3 modelos:

  • KNN1: KNN con las variables del Modelo 1

  • QDA1: QDA con las variables del Modelo 1

  • LDA1: LDA (Bis, ayudándole con las probabilidades a priori, sesgándole) con las variables del Modelo 1.

En términos de Accuracy, vemos que la mayoría de los modelos tienen una puntuación muy parecida, de un Accuracy de en torno al 60%. Vemos que modelos que en términos de Kappa no predecían bien, como pueden ser ambos Naive Bayes (dos de las puntuaciones más altas en términos de Accuracy) o Random Forest, que en Kappa tenía 0 y en términos de accuracy vemos que no queda muy por detrás del mejor modelo, KNN1.

Trading Strategy for the SP500:

En este apartado trateremos de desarrollar una estrategia lucrativa de trading con el SP500. Para ello utilizaremos los 3 mejores algoritmos en términos de Kappa que vimos en el apartado anterior. Para ello utilizaremos sus predicciones de probabilidad, en lugar del “TARGET” predicho como tal (queremos utilizar la probabilidad estimada por el modelo de que el mercado suba); lo que haremos será buscar el margen necesario para que dicha estrategia de trading sea lucrativa. En este apartado sí mostraremos la mayoría del código utilizado, pues puede resultar algo confuso de explicar en palabras (cosa que también haremos), y poder ver el código ayudará a la comprensión de las reglas que seguirá nuestra estrategia de inversión. Para esta sección, asumiremos que podemos ir tanto en largo (posición = 1) como en corto (posición = -1).
Para calcular el cumulative return, lo haremos descontando la tasa libre de riesgo (los bonos del tesoro americano), es decir, teniendo en cuenta el coste de oportunidad de la actividad financiera. Para ello utilizaremos la columna “CRSP_SPvwx”. Sin embargo, no tenemos en cuenta los costes en términos de intereses de ir en corto, así como las comisiones de mercado, pues ambos los desconocemos y no tenemos una forma fácil de estimarlo. Para un análisis más completo de lo lucrativa que podría resultar cualquiera de estas estrategias puestas en práctica, tendríamos que tener en cuenta ambos costes.

tresholds = c(0.05, 0.10, 0.15, 0.20, 0.25, 0.3, 0.35, 0.4, 0.45)

Para poder contrastar lo buenos que son los modelos a la hora de generar beneficios, los contrastaremos con la estrategia Buy&Hold, que veremos a continuación.

Buy and Hold Strategy

Vamos a visualizar la estrategia Buy&Hold, para poder comparar nuestros resultados con lo que habría supuesto utilizar un modelo para predecir y hacer trading.

Como podemos ver, el Buy&Hold Strategy da un rendimiento bastante pobre, por lo que es posible que varios de nuestros modelos lo superen.

Trading with KNN1:

En este sub-apartado, lo que haremos será simular una situación real en la que empezaremos a hacer trading a partir del momento en el que empieza el test. Recordemos que el momento de test empezaba el 1 de Febrero de 2006.

d3$date = dates

d3$CRSP_SPvwx = returns

which(d3$date=="2006-02-01")
## [1] 430
nrow(d3[430:nrow(d3), ])
## [1] 130
testdates = d3[430:nrow(d3), 'date']
CumRetDf = matrix(nrow=130, ncol=length(tresholds))

colnames(CumRetDf) = tresholds

j = 0

pos = 0

for(treshold in tresholds){
  
  j = j + 1
  
  nbuy=0
    
 
  nsold=0
  cumret = c(0)
  
  for(i in 430:(nrow(d3) - 1)){
    
    
    
    train = d3[1:i-1, ]
    
    train$TARGET = factor(train$TARGET)
    
    test = d3[i, ]
    
    knn <- train(mod1, method = "knn", 
                data = train,
                preProcess = c("center", "scale"),
                trControl = ctrl)

    knnpred = predict(knn, test, type="prob")[, 2]
    
    
    
    if(knnpred > (0.5 + treshold)){
      
      #print(paste("time to buy at date", as.character(d3[i, "date"])))
      
      pos = 1
      
      ret = pos*d3[i + 1, "CRSP_SPvwx"]
      
      retcum = cumret[length(cumret)] + ret
      
      cumret = c(cumret, retcum)
      
      nbuy= nbuy + 1
      
    #} else if(knnpred <= (0.5 + treshold) & pos >= 0){
      
    } else {
      
      #print(paste("time to sell at date", as.character(d3[i, "date"])))
      
      nsold = nsold + 1
      
      pos = -1
      
      ret = pos*d3[i+1, "CRSP_SPvwx"]
      
      retcum = cumret[length(cumret)] + ret
      
      cumret = c(cumret, retcum)
      
      
      }
      
  }
  print(paste("Con treshold ", as.character(treshold)," he vendido ", as.character(nsold), " veces, y he comprado ", as.character(nbuy), " veces"))
  CumRetDf[ , j] = cumret 
  }
## [1] "Con treshold  0.05  he vendido  17  veces, y he comprado  112  veces"
## [1] "Con treshold  0.1  he vendido  51  veces, y he comprado  78  veces"
## [1] "Con treshold  0.15  he vendido  53  veces, y he comprado  76  veces"
## [1] "Con treshold  0.2  he vendido  81  veces, y he comprado  48  veces"
## [1] "Con treshold  0.25  he vendido  88  veces, y he comprado  41  veces"
## [1] "Con treshold  0.3  he vendido  111  veces, y he comprado  18  veces"
## [1] "Con treshold  0.35  he vendido  111  veces, y he comprado  18  veces"
## [1] "Con treshold  0.4  he vendido  125  veces, y he comprado  4  veces"
## [1] "Con treshold  0.45  he vendido  125  veces, y he comprado  4  veces"
rets_knn = data.frame(CumRetDf)

rets_knn$date = testdates

gathered_knn = melt(rets_knn, id="date")

names(gathered_knn)[2] = "Treshold"

p = ggplot(gathered_knn, aes(x = date, y = value, group = Treshold, colour = Treshold)) +
  
  geom_line() +
  
  theme(axis.text.x = element_text(angle=60, vjust=0.5)) +
  
  ylab('Cumulative Return') +
  
  ggtitle("Cumulative Return for KNN with different Tresholds")

p

Parece bastante claro que lo mejor en este caso sería utilizar un treshold de 0.05, es decir, que si la predicción es que la probabilidad de que el precio suba es de más de un 55%, se compra el activo, y sino se vende. La rentabilidad es negativa, como vemos, por lo tanto una estrategia de trading basada en este algoritmo no creemos que pudiera funcionar. Un algoritmo para tener suficiente rentabilidad en la vida real debería tener un margen enorme en este test; pues parte del mismo se lo van a “comer” las comisiones y los intereses al ir en corto.

Trading with QDA1.

CumRetDf = matrix(nrow=130, ncol=length(tresholds))

colnames(CumRetDf) = tresholds

j = 0

pos = 0

for(treshold in tresholds){
  
  j = j + 1
  nbuy=0
  nsold=0
  cumret = c(0)
  
  for(i in 430:(nrow(d3) - 1)){
    
    
    train = d3[1:i-1, ]
    
    train$TARGET = factor(train$TARGET)
    
    test = d3[i, ]
    
    qda <- train(mod1, 
                # method = "lda", "lda2", "rda",
                method = "qda",
                data = train,
                preProcess = c("center", "scale"),
                trControl = ctrl)

    

    qdapred = predict(qda, test, type="prob")[, 2]
    
    
    
    if(qdapred > (0.5 + treshold)){
      
      #print(paste("time to buy at date", as.character(d3[i, "date"])))
      
      pos = 1
      
      ret = pos*d3[i + 1, "CRSP_SPvwx"]
      
      retcum = cumret[length(cumret)] + ret
      
      cumret = c(cumret, retcum)
      
      nbuy= nbuy + 1
      
    #} else if(knnpred <= (0.5 + treshold) & pos >= 0){
      
    } else {
      
      #print(paste("time to sell at date", as.character(d3[i, "date"])))
      
      nsold = nsold + 1
      
      pos = -1
      
      ret = pos*d3[i+1, "CRSP_SPvwx"]
      
      retcum = cumret[length(cumret)] + ret
      
      cumret = c(cumret, retcum)
      
      
      }
      
  }
  
  print(paste("Con treshold ", as.character(treshold)," he vendido ", as.character(nsold), " veces, y he comprado ", as.character(nbuy), " veces"))
   CumRetDf[ , j] = cumret 
  }
## [1] "Con treshold  0.05  he vendido  20  veces, y he comprado  109  veces"
## [1] "Con treshold  0.1  he vendido  22  veces, y he comprado  107  veces"
## [1] "Con treshold  0.15  he vendido  26  veces, y he comprado  103  veces"
## [1] "Con treshold  0.2  he vendido  30  veces, y he comprado  99  veces"
## [1] "Con treshold  0.25  he vendido  34  veces, y he comprado  95  veces"
## [1] "Con treshold  0.3  he vendido  45  veces, y he comprado  84  veces"
## [1] "Con treshold  0.35  he vendido  53  veces, y he comprado  76  veces"
## [1] "Con treshold  0.4  he vendido  67  veces, y he comprado  62  veces"
## [1] "Con treshold  0.45  he vendido  84  veces, y he comprado  45  veces"
rets_qda = data.frame(CumRetDf)

rets_qda$date = testdates

gathered_qda = melt(rets_qda, id="date")

names(gathered_qda)[2] = "Treshold"

p = ggplot(gathered_qda, aes(x = date, y = value, group = Treshold, colour = Treshold)) +
  
  geom_line() +
  
  theme(axis.text.x = element_text(angle=60, vjust=0.5)) +
  
  ylab('Cumulative Return') +
  
  ggtitle("Cumulative Return for QDA1 with different Tresholds") +
  
  labs(caption="A VACA ON DATA (alexvaca0.github.io)")

ggplotly(p)

Sin embargo parece que QDA1 es capaz de hacer muy buenos cumulative return con diferentes tresholds, siendo todas sus apuestas ganadoras (como vemos, todas las estrategias, sea cual sea el treshold, están por encima del 0% del cumulative return, en contraste con las del modelo anterior). De nuevo el mejor Treshold parece ser 0.05.

Trading with LDA 1(Bis)

CumRetDf = matrix(nrow=130, ncol=length(tresholds))

colnames(CumRetDf) = tresholds

j = 0

pos = 0

for(treshold in tresholds){
  
  j = j + 1
  
  cumret = c(0)
  
  nbuy=0
  nsold=0
  
  for(i in 430:(nrow(d3) - 1)){
    
    
    train = d3[1:i-1, ]
    
    train$TARGET = factor(train$TARGET)
    
    test = d3[i, ]
    
    lda = lda(mod1, data = train, prior = c(0.9, 0.1))

    ldapred = predict(lda, test)$posterior[,2]

    
    
    if(ldapred > (0.5 + treshold)){
      
      #print(paste("time to buy at date", as.character(d3[i, "date"])))
      
      pos = 1
      
      ret = pos*d3[i + 1, "CRSP_SPvwx"]
      
      retcum = cumret[length(cumret)] + ret
      
      cumret = c(cumret, retcum)
      
      nbuy= nbuy + 1
      
    #} else if(knnpred <= (0.5 + treshold) & pos >= 0){
      
    } else {
      
      #print(paste("time to sell at date", as.character(d3[i, "date"])))
      
      nsold = nsold + 1
      
      pos = -1
      
      ret = pos*d3[i+1, "CRSP_SPvwx"]
      
      retcum = cumret[length(cumret)] + ret
      
      cumret = c(cumret, retcum)
      
      
      }
      
  }
  
  print(paste("Con treshold ", as.character(treshold)," he vendido ", as.character(nsold), " veces, y he comprado ", as.character(nbuy), " veces"))
   CumRetDf[ , j] = cumret 
  }
## [1] "Con treshold  0.05  he vendido  128  veces, y he comprado  1  veces"
## [1] "Con treshold  0.1  he vendido  129  veces, y he comprado  0  veces"
## [1] "Con treshold  0.15  he vendido  129  veces, y he comprado  0  veces"
## [1] "Con treshold  0.2  he vendido  129  veces, y he comprado  0  veces"
## [1] "Con treshold  0.25  he vendido  129  veces, y he comprado  0  veces"
## [1] "Con treshold  0.3  he vendido  129  veces, y he comprado  0  veces"
## [1] "Con treshold  0.35  he vendido  129  veces, y he comprado  0  veces"
## [1] "Con treshold  0.4  he vendido  129  veces, y he comprado  0  veces"
## [1] "Con treshold  0.45  he vendido  129  veces, y he comprado  0  veces"
rets_lda = data.frame(CumRetDf)

rets_lda$date = testdates

gathered_lda = melt(rets_lda, id="date")

names(gathered_lda)[2] = "Treshold"

p = ggplot(gathered_lda, aes(x = date, y = value, group = Treshold, colour = Treshold)) +
  
  geom_line() +
  
  theme(axis.text.x = element_text(angle=60, vjust=0.5)) +
  
  ylab('Cumulative Return') +
  
  ggtitle("Cumulative Return for LDA with different Tresholds") +
  
  labs(caption="A VACA ON DATA (alexvaca0.github.io)")

ggplotly(p)

El motivo por el que no vemos la mayoría de puntos es porque están solapados por los demás, es decir, la mayoría de Tresholds dan un resultado bastante similar, mostrando que el Kappa mayor de este modelo no tenía nada que ver con una variedad adecuada en las predicciones en contraposición con los dos modelos mostrados anteriormente. Todos los modelos acaban con un cumulative return negativo. El que menos pierde es el LDA 1 con el Treshold en 0.4.

Visualización Final de la Estrategia de Trading

total_returns = data.frame(date=testdates, knn=rets_knn$X0.05, qda=rets_qda$X0.05, lda=rets_lda$X0.4, buyhold=cum_ret)

gath_rets = melt(total_returns, id="date")

names(gath_rets)[2] = "Strategy"

p = ggplot(gath_rets, aes(x = date, y = value, colour = Strategy, group=Strategy)) +
  
  geom_line() +
  
  theme(axis.text.x = element_text(angle=60, vjust=0.5)) +
  
  ylab('Cumulative Return') +
  
  ggtitle("Cumulative Return for the Different Strategies") +
  
  labs(caption="A VACA ON DATA (alexvaca0.github.io)")

ggplotly(p)

Como vemos en el gráfico superior,como QDA (este con mucha más claridad) supera el Buy&Hold Strategy. Podemos decir, pues, que haber utilizado QDA1 entre los años 2006 y 2017 (casi, Noviembre de 2016) habría sido lucrativo, al menos más que el mercado. Vemos de esta forma que se puede encontrar un modelo que dé unos resultados bastante aceptables para clasificar si sube o baja el precio, de mes a mes, del SP500, el cual podríamos comprar a través de un ETF o algún otro instrumento que trackee índices. El mercado de estos productos es líquido y por lo tanto podríamos establecer una estrategia de trading exitosa, vistos los resultados, con las limitaciones de no haber tenido en cuenta los costes de operar en el mercado, los cuales, de forma porcentual, esperamos que sea inferior a la diferencia que existe entre el Cumulative Return del QDA y el Cumulative Return del Buy&Hold. Estos costes tendrían una clara influencia sobre el treshold que hemos optimizado en esta última parte, ya que tendríamos que estar más seguros de que va a bajar el precio a la hora de ir en corto que de que va a subir para ir con posición = 1.